跳到主要内容

数据结构 四叉树 quadTree

包含的用例代码 simplequadtree 里面内含了一个绘图工具,方便打印四叉树

四叉树是什么?

二叉树的变体二叉查找树,非常适合用来进行对一维数列的存储和查找,可以达到 O(logn)O(logn) 的效率;我们在用二叉查找树进行插入数据时,根据一个数据的值和树结点值的对比,选择二叉树的两个叉之一向下,直到叶子结点,查找时使用二分法也可以迅速找到需要的数据。

但二叉树只支持一维数据,如一个标量数值,对地图上的位置点这种有xy两个方向上的信息却无能为力,那么是否有一种树能够支持二维数据的快速查询呢?

所以这时可以使用四叉树来完成两个轴向的查找(同理三维可以使用八叉树)

image0e5a634dab180b19.png

四叉树是种很直接的空间索引技术。在四叉树中,每个节点表示覆盖了部分进行索引的空间的边界框,根节点覆盖了整个区域。每个节点要么是叶节点,有包含一个或多个索引点的列表,没有孩子。要么是内部节点,有四个孩子,每个孩子对应将区域沿两根轴对半分得到的四个象限中的一个,四叉树也因此得名。

image69d6299b93383a0b.png

它为每个结点添加一个“容量”的属性,在四叉树初始化时只有一个根结点,在插入数据时,如果一个结点内的数据量大于了结点“容量”,再将结点进行分裂。如此,可以保证每个结点内都存储着数据,避免了内存空间的浪费。

在查询时,只有找到了位置对应的结点,那么结点下的所有点都会是此位置的附近点,更小的“容量”意味着每个结点内点越少,也就意味着查询的精度会越高。

四叉树位置点的插入流程如下图所示:

image97ce0630f8aff937.png

注意:只有叶子节点里面才会存在元素

定义结构

const MAX_ELE_NUM = 100

// Region 表示节点保存元素的范围
type Region struct {
up int
bottom int
left int
right int
}

// ElePoint 保存真实的数据
type ElePoint struct {
x int
y int
data string
}

// QuadTreeNode 一个四叉树节点,里面包含若干个元素
type QuadTreeNode struct {
depth int // 节点深度
isLeaf bool // 是否是叶子节点
region Region // 区域范围
LU *QuadTreeNode // 左上子结点指针
LB *QuadTreeNode // 左下子结点指针
RU *QuadTreeNode // 右上子结点指针
RB *QuadTreeNode // 右下子结点指针
eleNum int // 位置点数
eleList [MAX_ELE_NUM]*ElePoint // 位置点列表
}

如上,一个 Node 内部包含多个元素,该 Node 表示一个范围,当该 Node 下的节点大于最大元素数量则进行裂变操作

四叉树的操作

节点分裂

当满足特定条件时,为了获得更好的检索效率,四叉树的节点会发生分裂,分裂的做法是:以当前节点的矩形为中心,划分出四个等大的矩形,作为四个子节点,每个节点深度=当前节点深度+1,根节点深度为0;并且将当前节点的元素重新插入到对应的子节点。

/**
* 分裂结点
* 1.通过父结点获取子结点的深度和范围
* 2.生成四个结点,挂载到父结点下
*
* @param node
*/
func splitNode(node *QuadTreeNode) {
midVertical := (node.region.up + node.region.bottom) / 2
midHorizontal := (node.region.left + node.region.right) / 2
node.isLeaf = false

node.RU = newChildNode(node, midVertical, node.region.up, midHorizontal, node.region.right)
node.LU = newChildNode(node, midVertical, node.region.up, node.region.left, midHorizontal)
node.RB = newChildNode(node, node.region.bottom, midVertical, midHorizontal, node.region.right)
node.LB = newChildNode(node, node.region.bottom, midVertical, node.region.left, midHorizontal)

// 遍历结点下的位置点,将其插入到子结点中
for i := 0; i < node.eleNum; i++ {
InsertEle(node, *node.eleList[i])
node.eleList[i] = nil // 释放空间
node.eleNum--
}
}

插入元素

平面的元素多种多样,点,线,图形,但都能够做一个统一,第一步都是要找出图形所覆盖的节点,这里以矩形为例

从根节点开始,如果当前节点无子节点,将元素插入到当前节点。如果存在子节点K,并且元素能够完全被子节K点所“包围”,就将元素插入到子节点K,对于子节点K进行上述递归操作;如果元素跨越了多个子节点,那就将元素存储在当前节点

如果某一节点元素超过特定值,并且深度小于四叉树允许的最大深度,分裂当前节点。

/**
* 插入元素
* 1.判断是否已分裂,已分裂的选择适合的子结点,插入;
* 2.未分裂的查看是否过载,过载的分裂结点,重新插入;
* 3.未过载的直接添加
*
* @param node
* @param ele
*/
func InsertEle(node *QuadTreeNode, ele ElePoint) {
if node.isLeaf {
// 如果该节点包含的元素已经大于最大的元素数量则分裂节点
if node.eleNum+1 > MAX_ELE_NUM {
splitNode(node)
InsertEle(node, ele)
} else {
elePtr := &ElePoint{}
elePtr.y = ele.y
elePtr.x = ele.x
elePtr.data = ele.data
node.eleList[node.eleNum] = elePtr
node.eleNum++
}
return
}

midVertical := (node.region.up + node.region.bottom) / 2
midHorizontal := (node.region.left + node.region.right) / 2

// 把元素插入到对应的节点
if ele.y > midVertical {
if ele.x > midHorizontal {
InsertEle(node.RU, ele)
} else {
InsertEle(node.LU, ele)
}
} else {
if ele.x > midHorizontal {
InsertEle(node.RB, ele)
} else {
InsertEle(node.LB, ele)
}
}
}

检索

对于给定检索区域,从根节点起,如果检索区域与当前节点有交集,返回当前节点的所有元素。

如果当前节点还有子节点,递归检索与检索区域有交集的子节点。

// 找到元素的所在的 Node
func QueryNodeByElement(node *QuadTreeNode, ele *ElePoint) {
fmt.Printf("%+v %+v %+v %+v \n\n", node.depth, node.region, node.isLeaf, node.eleNum)
if node.isLeaf {
fmt.Printf("当前 Node 附近点有 %d 个,分别是:\n", node.eleNum)
for i := 0; i < node.eleNum; i++ {
// fmt.Printf("(%f,%f) \n", node.eleList[i].x, node.eleList[i].y)
}
return
}

midVertical := (node.region.up + node.region.bottom) / 2
midHorizontal := (node.region.left + node.region.right) / 2

if ele.y > midVertical {
if ele.x > midHorizontal {
QueryNodeByElement(node.RU, ele)
} else {
QueryNodeByElement(node.LU, ele)
}
} else {
if ele.x > midHorizontal {
QueryNodeByElement(node.RB, ele)
} else {
QueryNodeByElement(node.LB, ele)
}
}
}

References

空间管理Space management:四叉树 & 八叉树 四叉树应用于地图(点聚合)、碰撞检测等问题 空间索引 - 四叉树